/**
 * Copyright (c) 2008, Mu Dynamics.
 *  All rights reserved.
 *  
 *   Redistribution and use in source and binary forms, with or without modification, 
 *   are permitted provided that the following conditions are met:
 *   
 *  - Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice, 
 *     this list of conditions and the following disclaimer in the documentation and/or 
 *     other materials provided with the distribution.
 *  - Neither the name of the "Mu Dynamics" nor the names of its contributors may be used 
 *     to endorse  or promote products derived from this software without specific prior 
 *     written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.mu.jacob.core.builder;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Map.Entry;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import com.mu.jacob.core.generator.GeneratorException;
import com.mu.jacob.core.model.IModel;
import com.mu.jacob.core.model.decorator.Filter;
import com.mu.jacob.core.renderer.ContextTools;
import com.mu.jacob.core.renderer.IRenderer;
import com.mu.jacob.core.util.FileUtils;

/**
 * Element builder generates one file for every model element.
 * Property <code>element.kind</code> defines the type of the
 * element, for example package, interface or class (default). 
 * For more specific selection use <code>element.filter</code> attribute
 * and define the filter using  Velocity macro. Access filtered element 
 * using <code>element</code> context alias, for example
 * ${element.simpleName.startsWiht(\"Generator\")}
 * The output file names are generated using the a Velocity marco
 * defined using <code>output.file.template</code> property.
 * Here as well the element can be accesses using <code>element</code>
 * context alias.
 * 
 * @author Adam Smyczek
 *
 */
public class ElementBuilder extends AbstractBuilder {

	public static final String PROPERTY_ELEMENT_TYPE = "element.kind";

	public static final String PROPERTY_ELEMENT_FILTER = "element.filter";

	public static final String PROPERTY_OUTPUT_FILE_TEMPLATE = "output.file.template";
	
	public static final String CONTEXT_ELEMENT = "element";
	
	/* Velocity engine */
	private VelocityEngine engine;
	
	/* Rendered element kind */
	private ElementKind elementKind = null;
	
	/* Filter velocity template */
	private String filterTemplate = null;
	
	/* Velocity template generated file names */
	private String outputFilenameTemplate = null;
	
	/**
	 * Default constructor initializes
	 * the velocity engine for filter and output file generation
	 */
	public ElementBuilder() {
		super();
		initVelocityEngine();
	}
	
	/**
	 * Use this constructor when initializing builder for JacobRunner
	 * @param elementKind
	 * @param outputFilenameTemplate velocity output file name template, for example ${element.simpleName}.java
	 */
	public ElementBuilder(final IRenderer renderer, final String templateFile, final String templateRoot, 
									 final ElementKind elementKind, final String outputFilenameTemplate) {
		super(renderer, templateFile, templateRoot);
		initVelocityEngine();
		this.elementKind = elementKind;
		this.outputFilenameTemplate = outputFilenameTemplate;
	}
	
	/**
	 * Initialize velocity engine for filter and output file rendering
	 */
	private void initVelocityEngine() {
		try {
			engine = new VelocityEngine();
			engine.init();
		} catch (Exception e) {
			throw new GeneratorException("Unable to parse parameters. Velocity engine initialization failed!", e);
		}
	}
	
	/**
	 * Sets velocity filter template for element selection
	 * @param setFilterTemplate
	 */
	public void String(final String filterTemplate) {
		this.filterTemplate = filterTemplate;
	}
	
	/**
	 * @param model
	 * @param element kind
	 * @return element set based on defined element kind
	 */
	private Set<Element> getElementSet(IModel model) {
		// get element kind list
		Set<Element> elementSet = new HashSet<Element>();
		switch(getElementKind()) {
		case PACKAGE:
			elementSet.addAll(model.getPackages());
			break;
		default:
			elementSet.addAll(Filter.filterElements(model.getRootElements(), TypeElement.class, getElementKind()));
			break;
		}
		return elementSet;
	}
	
	/**
	 * Filters element set using defined Velocity template
	 * @param originElementSet
	 * @return filtered element set
	 */
	private Set<Element> filterElementSet(final Set<Element> originElementSet, final String filterTemplate) {
		Set<Element> elementSet = new HashSet<Element>();
		Iterator<Element> iter = originElementSet.iterator();
		while(iter.hasNext()) {
			Element element = iter.next();
			
			VelocityContext context = new VelocityContext();
			context.put("element", element);
			context.put(IRenderer.CONTEXT_TOOLS, ContextTools.getInstance());
			
			StringWriter writer = new StringWriter();
			try {
				if (!engine.evaluate(context, writer, "element.filter.log", new StringReader(filterTemplate))) {
					throw new GeneratorException("Unable to parse element.filter parameter!");
				}
			} catch (Exception e) {
				throw new GeneratorException("Unable to parse element.filter parameter!");
			}
			
			if ((writer.toString() != null) && ("true".equals(writer.toString()))) {
				elementSet.add(element);
			}
		}
		return elementSet;
	}
	
	/**
	 * Internal build implementation
	 * @param model
	 * @param elementSet
	 */
	private void build(IModel model, Set<Element> elementSet) {
		IRenderer renderer = getRenderer();

		// generate files
		Element catchElement = null;
		try {
			for (Element element : elementSet) {
				catchElement = element;
				
				File outputFile = FileUtils.getOrCreateFile(new File(getBaseDir(), pareseFileName(element, getOutputFileNameTemplate())));
				File templateFile = new File(getTemplateRoot(), prop(PROPERTY_TEMPLATE_FILE));
				
				for (Entry<String, Object> e : getContextMap().entrySet()) {
					renderer.addContext(e.getKey(), e.getValue());
				}
				renderer.addContext(CONTEXT_ELEMENT, element)
					.addContext(IRenderer.CONTEXT_BUILDER, this)
					.addContext(IRenderer.CONTEXT_MODEL, model);
				
				String result = renderer.render(templateFile);
		
		        BufferedWriter out = new BufferedWriter(new FileWriter(outputFile));
		        out.write(result);
		        out.close();

			}
		} catch (Exception e) {
			throw new GeneratorException("Unable to build types! Failed processing " + catchElement, e);
		}
	}
	
	/**
	 * Build method
	 */
	public void build(IModel model) {
		String template = getFilterTemplate();
		if (template != null)  {
			build(model, filterElementSet(getElementSet(model), template));
		} else {
			build(model, getElementSet(model));
		}
	}

	/**
	 * Generate output file name for element using defined output file name template
	 * @param element
	 * @param outputFileTemplate
	 * @return output file name
	 * @throws Exception in case of file name template is not defined or could not be rendered
	 */
	protected String pareseFileName(Object element, String outputFileTemplate) throws Exception {
		if (outputFileTemplate == null) {
			throw new GeneratorException("Output file template not set!");
		}

		VelocityContext context = new VelocityContext();
		context.put("element", element);
		
		StringWriter writer = new StringWriter();
		if (!engine.evaluate(context, writer, "output.file.template.log", new StringReader(outputFileTemplate))) {
			throw new GeneratorException("Unable to parse output.file.template output template!");
		}
		
		return writer.toString();
	}

	/**
	 * @return element kind defining the rendered element type,
	 * for example package, interface or class
	 */
	public ElementKind getElementKind() {
		if (elementKind == null) {
			String kind = getProperties().getProperty(PROPERTY_ELEMENT_TYPE);
			if (kind != null) {
				try {
					elementKind = ElementKind.valueOf(kind.toUpperCase());
				} catch (Exception e) {
					throw new GeneratorException("Invalid element kind " + kind, e);
				}
			}
			if (elementKind == null) {
				elementKind = ElementKind.CLASS;
			}
		}
		return elementKind;
	}
	
	/**
	 * @return velocity filter template 
	 */
	public String getFilterTemplate() {
		if (filterTemplate == null) {
			filterTemplate = getProperties().getProperty(PROPERTY_ELEMENT_FILTER);
		}
		return filterTemplate;
	}

	/**
	 * @return velocity template for output file names
	 */
	public String getOutputFileNameTemplate() {
		if (outputFilenameTemplate == null) {
			outputFilenameTemplate = getProperties().getProperty(PROPERTY_OUTPUT_FILE_TEMPLATE);
		}
		return outputFilenameTemplate;
	}
}
